using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace Common.Compression
{
    /// <summary>
    /// A compressed stream that's compatible with Zlib, as defined in RFC1950
    /// </summary>
    public class ZlibStream : DeflateStream
    {
        /// <summary>
        /// Create a new Zlib stream wrapper around the given stream.
        /// </summary>
        /// <param name="s">underlying data</param>
        /// <param name="mode">whether to compress (write into that stream) or decompress (read from that stream)</param>
        public ZlibStream(Stream s, CompressionMode mode)
            : this(s, mode, false)
        { }

        /// <summary>
        /// Create a new Zlib stream wrapper around the given stream.
        /// </summary>
        /// <param name="s">underlying data</param>
        /// <param name="mode">whether to compress (write into that stream) or decompress (read from that stream)</param>
        /// <param name="leaveOpen">should we leave the underlying stream open afterwards?</param>
        public ZlibStream(Stream s, CompressionMode mode, bool leaveOpen)
            : base(s, mode, true)
        {
            m_mode = mode;
            m_stream = s;
            m_fLeaveOpen = leaveOpen;
            switch (mode)
            {
                case CompressionMode.Decompress:
                    // chomp up the two bytes.
                    byte b1 = (byte)s.ReadByte(), b2 = (byte)s.ReadByte();
                    // check that the method is as expected.
                    int iCompressionMethod = (int)(b1 & 0xF);
                    if (iCompressionMethod != 8)
                    {
                        throw new Exception("Unknown compression method " + iCompressionMethod + "; this decoder only implements DEFLATE");
                    }
                    // read bytes -- make sure match required data format.
                    uint u = 256 * ((uint)b1) + ((uint)b2);
                    if (u % 31 != 0)
                    {
                        throw new Exception("Bad Zlib header -- lead bytes aren't a multiple of 31.");
                    }
                    // ensure that they aren't expecting a dictionary -- we can't handle one.
                    if ((b2 & 32) != 0)
                    {
                        throw new Exception("Unsupported Zlib feature in stream; stream requests dictionary, but we don't support them.");
                    }
                    break;

                case CompressionMode.Compress:
                    // write out two header bytes.
                    //  CM=8
                    //  CMINFO=7
                    //  FLEVEL=1
                    //  FDICT=0
                    // that gives us base value of
                    // 0111 1000 for the first byte  = 7 * 16 + 8 = 120
                    // 0100 0000 for the second byte = 64
                    // that equals 30784 as an int.  add 30 to make it a multiple of 31
                    // 0111 1000 = 120
                    // 0101 1110 = 94
                    s.WriteByte(120);
                    s.WriteByte(94);
                    break;

                default:
                    throw new Exception("Unknown compression mode " + mode);
            }
        }

        public override IAsyncResult BeginWrite(byte[] array, int offset, int count, AsyncCallback asyncCallback, object asyncState)
        {
            throw new Exception("async not implemented.");
        }

        public override void Write(byte[] array, int offset, int count)
        {
            m_uAdler = AdlerUpdate(array, offset, count, m_uAdler);
            base.Write(array, offset, count);
        }

        protected override void Dispose(bool disposing)
        {
            try
            {
                base.Dispose(disposing);
                if (m_mode == CompressionMode.Compress && m_stream != null)
                    WriteAdler();
            }
            finally
            {
                try
                {
                    if (disposing && !m_fLeaveOpen && m_stream != null)
                        m_stream.Close();
                }
                finally
                {
                    m_stream = null;
                }
            }
        }

        private void WriteAdler()
        {
            byte[] rgb = new byte[4];
            rgb[0] = (byte)((m_uAdler >> 24) & 0xff);
            rgb[1] = (byte)((m_uAdler >> 16) & 0xff);
            rgb[2] = (byte)((m_uAdler >> 8) & 0xff);
            rgb[3] = (byte)((m_uAdler >> 0) & 0xff);
            m_stream.Write(rgb, 0, 4);
        }

        protected uint AdlerUpdate(byte[] array, int offset, int count, uint adler)
        {
            const uint BASE = 65521;

            uint s1 = adler & 0xffffu;
            uint s2 = (adler >> 16) & 0xffffu;

            for (int n = 0; n < count; ++n)
            {
                s1 = (s1 + array[n + offset]) % BASE;
                s2 = (s2 + s1) % BASE;
            }

            return (s2 << 16) + s1;
        }

        protected CompressionMode m_mode;
        protected Stream m_stream;
        protected uint m_uAdler = 1;
        protected bool m_fLeaveOpen;
        // from RFC1950 -- 
        // A zlib stream has the following structure:
        //      0   1
        //    +---+---+
        //    |CMF|FLG|   (more-->)
        //    +---+---+
        // (if FLG.FDICT set)
        //      0   1   2   3
        //    +---+---+---+---+
        //    |     DICTID    |   (more-->)
        //    +---+---+---+---+
        //    +=====================+---+---+---+---+
        //    |...compressed data...|    ADLER32    |
        //    +=====================+---+---+---+---+
        // Any data which may appear after ADLER32 are not part of the zlib
        // stream.
        // 
        // CMF (Compression Method and flags)
        //    This byte is divided into a 4-bit compression method and a 4-
        //    bit information field depending on the compression method.
        // 
        //       bits 0 to 3  CM     Compression method
        //       bits 4 to 7  CINFO  Compression info
        // 
        // CM (Compression method)
        //    This identifies the compression method used in the file. CM = 8
        //    denotes the "deflate" compression method with a window size up
        //    to 32K.  This is the method used by gzip and PNG (see
        //    references [1] and [2] in Chapter 3, below, for the reference
        //    documents).  CM = 15 is reserved.  It might be used in a future
        //    version of this specification to indicate the presence of an
        //    extra field before the compressed data.
        // 
        // CINFO (Compression info)
        //    For CM = 8, CINFO is the base-2 logarithm of the LZ77 window
        //    size, minus eight (CINFO=7 indicates a 32K window size). Values
        //    of CINFO above 7 are not allowed in this version of the
        //    specification.  CINFO is not defined in this specification for
        //    CM not equal to 8.
        // 
        // FLG (FLaGs)
        //    This flag byte is divided as follows:
        // 
        //       bits 0 to 4  FCHECK  (check bits for CMF and FLG)
        //       bit  5       FDICT   (preset dictionary)
        //       bits 6 to 7  FLEVEL  (compression level)
        // 
        //    The FCHECK value must be such that CMF and FLG, when viewed as
        //    a 16-bit unsigned integer stored in MSB order (CMF*256 + FLG),
        //    is a multiple of 31.
        // 
        // FDICT (Preset dictionary)
        //    If FDICT is set, a DICT dictionary identifier is present
        //    immediately after the FLG byte. The dictionary is a sequence of
        //    bytes which are initially fed to the compressor without
        //    producing any compressed output. DICT is the Adler-32 checksum
        //    of this sequence of bytes (see the definition of ADLER32
        //    below).  The decompressor can use this identifier to determine
        //    which dictionary has been used by the compressor.
        // 
        // FLEVEL (Compression level)
        //    These flags are available for use by specific compression
        //    methods.  The "deflate" method (CM = 8) sets these flags as
        //    follows:
        // 
        //       0 - compressor used fastest algorithm
        //       1 - compressor used fast algorithm
        //       2 - compressor used default algorithm
        //       3 - compressor used maximum compression, slowest algorithm
        // 
        //    The information in FLEVEL is not needed for decompression; it
        //    is there to indicate if recompression might be worthwhile.
        // 
        // compressed data
        //    For compression method 8, the compressed data is stored in the
        //    deflate compressed data format as described in the document
        //    "DEFLATE Compressed Data Format Specification" by L. Peter
        //    Deutsch. (See reference [3] in Chapter 3, below)
        // 
        //    Other compressed data formats are not specified in this version
        //    of the zlib specification.
    }
}
